﻿

//本文件完成对贪吃蛇游戏各函数的实现
#include"Snake.h"


//游戏开始  完成游戏初始化工作
  /* 1.设置游戏窗⼝的⼤⼩
	 2. 设置窗⼝的名字
     3. 隐藏屏幕光标
     4. 打印欢迎界⾯ - WelcomeToGame
	 5. 创建地图 - CreateMap
	 6. 初始化蛇⾝ - InitSnake
	 7. 创建⻝物 - CreateFood*/

//定位光标位置
void SetPos(int x, int y) {
	//获取标准输出设备句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定义坐标变量
	COORD pos = { x,y };
	//定位控制台光标位置
	SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎界面同时功能介绍
void WelcomeToGame() {
	//将光标定位在屏幕中心 再打印信息
	SetPos(40, 10);
	printf("欢迎来到贪吃蛇小游戏!\n");
	SetPos(40, 15);
	system("pause");
	//第一个欢迎界面显示后，按任意键清屏
	system("cls");
	//清屏后展示第二个界面，介绍游戏功能
	SetPos(20, 10);
	printf("玩法介绍：");
	SetPos(30, 10);
	printf("·通过按键↑←↓→来控制蛇的移动，按F3加速，F4减速");
	SetPos(30, 11);
	printf("·加速后，每个食物的分数会更高");
	SetPos(30, 12);
	printf("·每吃一个食物会获得相应分数，蛇身会变长");
	SetPos(30, 13);
	printf("·贪吃蛇头触碰到墙或自身时，游戏结束");
	SetPos(30, 20);
	//按任意键后，清屏第二个界面
	system("pause");
	system("cls");
	//第三个界面，游戏界面

}
//定义墙体符号
#define WALL L'□'
//绘制游戏地图
void CreateMap() {
//棋盘27行 58列
	           
	//最上面一行 打印29个宽字符    光标初始就在（0，0）直接向后打印即可
	for (int i = 0;i < 29;i++) {
		wprintf(L"%lc", WALL);
	}  
	//最下面一行                将光标定位在(0,26)开始向后打印即可
	SetPos(0, 26);
	for (int i = 0;i < 29;i++) {
		wprintf(L"%lc", WALL);
	}
	//最左一行      初始光标位置在（0，1）每打印一个墙体符号都要重新定位光标（因为是在第一列，可通过换行来定位光标）,打印25次即可
	SetPos(0, 1);
	for (int i = 0;i < 25;i++) {
		wprintf(L"%lc\n", WALL);

	}
	//最右一行               初始光标位置在（56，1）每打印一个墙体符号重新定位光标，打印25次
	for (int i = 1;i <= 25;i++) {
		SetPos(56, i); //将循环变量与光标的纵坐标结合起来定位光标
		wprintf(L"%lc", WALL);
	}
}
//创建贪吃蛇节点
pSnakeNode CreateSnakeNode(int x, int y) {
	pSnakeNode newNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (newNode == NULL) {
		perror("CreateSnakeNode()::malloc"); //创建新节点失败，打印错误发生的位置及原因
		return NULL;
	}
	newNode->x = x;
	newNode->y = y;
	newNode->next = NULL;
	return newNode;
 }
//定义贪吃蛇初始蛇身尾节点横纵坐标
#define POS_X 24
#define POS_Y  5
//定义蛇身符号
#define BODY  L'●'
//初始化贪吃蛇   初始长度5，在屏幕固定位置出现
void InitSnake(pSnake ps) {
	//初始移动方向向右
	ps->_Dir = RIGHT;
	//初始每个食物分数为10
	ps->_FoodWeight = 10;
	//初始总分为0
	ps->_Score = 0;
	//初始每移动一步休眠200毫秒
	ps->_SleepTime = 200; 
	//初始状态正常运行
	ps->_Status = OK;
	//初始蛇头指针为空
	ps->_pSnake = NULL;

	//创建蛇身 即贪吃蛇节点 ,初始蛇身长为5 尾节点坐标(24,5)
	for (int i = 0;i < 5;i++) {
		pSnakeNode pcur = CreateSnakeNode(POS_X+i*2,POS_Y);//从尾节点开始创建，下一个节点横坐标+2
		if (pcur != NULL) { //节点创建成功
			//头插法创建贪吃蛇
			pcur->next = ps->_pSnake;
			ps->_pSnake = pcur;//更新贪吃蛇头指针
		}
	}
	//贪吃蛇链表构建完毕
	//打印贪吃蛇 遍历贪吃蛇节点
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);//将光标定位到节点所占位置，再打印蛇身符号
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
}
//定义食物符号
#define FOOD L'★'
//创建食物
void CreateFood(pSnake ps) {
	//随机生成食物节点的坐标
		//食物节点坐标的特点：1.横坐标x必须是2的倍数，确保蛇身能完全覆盖掉食物，因为蛇身一个符号占2个字节
		//                    2.食物坐标不能与蛇身节点坐标重合
	//修改随机数种子,确保得到的是真随机数
	srand((unsigned int)time(NULL));
	int x = 0, y = 0;
 again:
	do {
		x = rand() % 53 + 2;       //x 2~54   0~52 +2
		y = rand() % 25 + 1;         //y 1~25  0~24 +1
		
	}while( x%2!=0); //如果x不是2的倍数就重新生成x y
	//x满足了第一个要求，遍历蛇身节点，确保x、y不与蛇身节点重合，否则重新生成食物坐标
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		if (cur->x == x && cur->y == y) { //如果食物坐标与蛇身坐标有重合，重新生成食物坐标
			goto again;
			break;
		}
		cur = cur->next;
	}
	//创建食物（贪吃蛇节点），并打印到屏幕
	pSnakeNode food = CreateSnakeNode(x,y);
	if (food != NULL) {
		ps->_Food = food;  //贪吃蛇通过指针来维护食物节点
		SetPos(ps->_Food->x, ps->_Food->y);
		wprintf(L"%lc", FOOD);
	}

}
void GameStart(pSnake ps) {
	//1.设置游戏窗口大小    棋盘27行 58列 ，将窗口设为100列，30行
	system("mode con cols=100 lines=30");
	//2. 设置窗⼝的名字
	system("title 贪吃蛇");
	//3.隐藏屏幕光标
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursor_info;
   GetConsoleCursorInfo(hOutput, &cursor_info);
   cursor_info.bVisible = false;
    SetConsoleCursorInfo(hOutput, &cursor_info);
	//4.打印欢迎界面同时功能介绍
	WelcomeToGame();
	//5.进入游戏界面，创建地图 - CreateMap
	CreateMap();
	//6.初始化蛇⾝ - InitSnake
	InitSnake(ps);
	//7.创建食物
	CreateFood(ps);


	//SetPos(60, 26);
}


//游戏运行
//1.打印地图右侧帮助信息
// 2.按键检测，并作出相应响应
//3.贪吃蛇移动  下一个节点是食物|下一个节点不是食物
//4.状态检测 是否撞墙、撞到自己、正常退出

//打印地图右侧帮助信息 游戏功能提示
void PrintHelpInfo() {
	

	SetPos(65, 10);
	printf("·用按键↑←↓→来控制蛇的移动");
	SetPos(65, 11);
	printf("·贪吃蛇不可以撞墙和撞到自己");
	SetPos(65, 12);
	printf("·F3加速，F4减速");
	SetPos(65, 13);
	printf("·空格暂停，ESC退出");
}
//定义一个函数宏，用来检测某键值对应按键是否被按过，结果为1表示被按过，为0表示未被按过
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&1)?1:0)

//游戏暂停
void GamePause() {
	/*SetPos(30, 15);
	printf("游戏已暂停，按空格继续");*/
	while (KEY_PRESS(VK_SPACE) != 1) {
		Sleep(100);  
	}
}
//吃掉食物处理
void EatFood(pSnake ps) {
	//将食物节点挂到贪吃蛇头
	ps->_Food->next = ps->_pSnake;
	//更新头指针
	ps->_pSnake = ps->_Food;
	//更新总分数
	ps->_Score += ps->_FoodWeight;

	//贪吃蛇更新后，重新打印贪吃蛇
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}
//下一个节点不是食物，贪吃蛇移动到此节点
void EatNext(pSnake ps,pSnakeNode pnext) {
	//下一个节点头插到贪吃蛇中
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;
	//释放当前贪吃蛇尾节点，并用两个空格将它之前打印出的符号覆盖掉
	pSnakeNode cur = ps->_pSnake;
	//找到尾节点的前一个节点，因为释放尾节点后要把它前一个节点的next指针置空
	while (cur->next->next) {
		cur = cur->next;
	}
	pSnakeNode del = cur->next; //del指向尾节点 ,cur指向倒数第二个节点
	//打印空格覆盖尾节点之前打印的符号
	SetPos(del->x, del->y);
	printf("  ");
	//释放尾节点，并置空
	free(del);
	del = NULL;
	//修改新尾节点next指针
	cur->next = NULL;
	//打印更新后的贪吃蛇
	 cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}
//贪吃蛇移动 
void SnakeMove(pSnake ps) {
	//根据当前贪吃蛇头节点的坐标即移动方向  获得下一个节点的坐标
	pSnakeNode pNext = NULL;
	switch (ps->_Dir) {
	case UP:
		pNext = CreateSnakeNode(ps->_pSnake->x, ps->_pSnake->y - 1);  //当前向上移动 下一个节点坐标(x,y-1)
		break;
	case DOWN:
		pNext = CreateSnakeNode(ps->_pSnake->x, ps->_pSnake->y +1);  //当前向下移动 下一个节点坐标(x,y+1)
		break;
	case LEFT:
		pNext = CreateSnakeNode(ps->_pSnake->x-2, ps->_pSnake->y ); //当前向左移动 下一个节点坐标(x-2,y)
		break;
	case RIGHT:
		pNext = CreateSnakeNode(ps->_pSnake->x+2, ps->_pSnake->y); //当前向右移动 下一个节点坐标(x+2,y)
		break;
	}
//判断下一个节点是否是食物
	if (pNext != NULL) {
		if (pNext->x == ps->_Food->x && pNext->y == ps->_Food->y) {
			//是食物，吃掉食物，贪吃蛇长度+1，总分数增加
			EatFood(ps);
			//吃掉食物后，随机生成新的食物
			CreateFood(ps);
		}
		else {
			//不是食物，贪吃蛇移动到此位置，长度保持不变，蛇尾节点释放
			EatNext(ps, pNext);
		}
	}
	
}
//是否撞墙检测
void KillByWall(pSnake ps) {
	//判断当前头节点的坐标即可完成贪吃蛇是否撞墙的判断
	//x坐标为0或56，则撞了左右墙 y坐标为0或26，则撞了上下墙
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26) {
		ps->_Status = KILL_BY_WALL;
	}
}
//是否撞到自身
void KillBySelf(pSnake ps) {
	pSnakeNode cur = ps->_pSnake->next;
	//从第二个节点开始遍历蛇身节点，判断蛇头坐标是否和某个蛇身节点坐标相等，有相等说明撞到了自身
	while (cur) {
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y) {
			ps->_Status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}
void GameRun(pSnake ps) {
	//打印地图右侧帮助信息
	PrintHelpInfo();
	//按键检测  在游戏正常运行时，循环检测按键状态
	do {
		//右侧打印当前总分数和食物分值
	SetPos(65, 7);
	printf("·总分数:%d",ps->_Score);
	SetPos(65, 8);
	printf("·当前食物分数:%2d",ps->_FoodWeight); //打印食物分数时占位符限定最小宽度为2，因为食物分数设定在2-18之间，如果不限定最小宽度，分数为是1位数时会显示有问题

		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN) { //判断↑键是否被按下，并且当前贪吃蛇运动方向不能为下
			ps->_Dir = UP;
	   }
		if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP) {
			ps->_Dir = DOWN;
		}
		if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT) {
			ps->_Dir = LEFT;
		}
		if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT) {
			ps->_Dir = RIGHT;
		}
		if (KEY_PRESS(VK_F3)) {    //按过F3 加速  最多加速4档
			if(ps->_FoodWeight < 18) { 
				ps->_FoodWeight += 2;
				ps->_SleepTime -= 20;
			}
		}
		if (KEY_PRESS(VK_F4)) {    //按过F4 减速   最多减速4档
			if (ps->_FoodWeight > 2) {
				ps->_FoodWeight -= 2;
				ps->_SleepTime += 20;
			}
		}
		if (KEY_PRESS(VK_ESCAPE) ) {   //如果按过ESC，游戏正常结束
			ps->_Status = END_NORMAL;
		}
		if (KEY_PRESS(VK_SPACE)) {  //如果按下过空格键，就将游戏暂停
			 GamePause();
		}
		//贪吃蛇移动
		SnakeMove(ps);
		Sleep(ps->_SleepTime);
		//撞墙、撞自身检测
		KillByWall(ps);
		KillBySelf(ps);

	} while (ps->_Status == OK);

	//游戏运行结束
	//1.正常退出
	//2.撞墙结束
	//3.撞自身结束

}
//游戏结束阶段
//1.显示结束原因
//2.销毁贪吃蛇链表

//销毁贪吃蛇 ，释放蛇身节点空间
void DestroySnake(pSnake ps) {
	//从贪吃蛇头节点开始删除，用cur释放当前节点，用pnext记录下一个要删除节点地址
	pSnakeNode pnext = ps->_pSnake;
	pSnakeNode cur =ps->_pSnake;
	while (cur) {
		 pnext = pnext->next;
		 free(cur);
		 cur = pnext;
	}
	//循环结束 pnext和cur都为NULL

}
void GameEnd(pSnake ps) {
	//1.显示结束原因
	if (ps->_Status == END_NORMAL) {
		SetPos(20,15);
		printf("游戏已正常退出");
	}
	if (ps->_Status == KILL_BY_WALL) {
		SetPos(20, 15);
		printf("贪吃蛇撞墙了，游戏失败");
	}
	if (ps->_Status == KILL_BY_SELF) {
		SetPos(20, 15);
		printf("贪吃蛇撞到了身体，游戏失败");
	}

	//销毁贪吃蛇 ，释放蛇身节点空间
	DestroySnake(ps);
}